/*
 * Copyright (C) 2012-2013  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.launch.internal;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.ParseException;

import nemos.launch.progress.IProgressListener;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.service.log.LogService;
import org.osgi.util.tracker.ServiceTracker;

/**
 * Recorder : The class which ensure current launch is recorded and listeners
 * are notified.
 * 
 * @author Romain DUBOIS
 */
public class Recorder {

    /** The felix fileinstall poll property */
    private static final String FELIX_FILEINSTALL_POLL = "felix.fileinstall.poll"; //$NON-NLS-1$
    /** Default file path for error messages */
    private static final String NULL = "<null>"; //$NON-NLS-1$
    /** The records file encoding */
    private static final String RECORDS_ENCODING = "UTF8"; //$NON-NLS-1$
    /** The records save file name */
    private static final String RECORDS_FILE_NAME = "records"; //$NON-NLS-1$
    /** The target start level property name */
    private static final String TARGET_LEVEL_PROPERTY = "nemos.launch.level"; //$NON-NLS-1$
    /** The initialization time property name */
    private static final String INIT_TIME_PROPERTY = "nemos.launch.firstInitTime"; //$NON-NLS-1$
    /** The step time property name */
    private static final String STEP_TIME_PROPERTY = "nemos.launch.firstStepTime"; //$NON-NLS-1$

    /** The bundle context */
    private final BundleContext mContext;
    /** The listeners service tracker */
    private final ServiceTracker<IProgressListener, IProgressListener> mListenersTracker;
    /** The previous launch records data object */
    private RecordsImpl mRecords;
    /** The current status */
    private StatusImpl mStatus;
    /** The first initialization waiting time */
    private long mInitTime;
    /** The first steps waiting time */
    private long mStepTime;
    /** The framework launch level */
    private int mLaunchLevel;

    /**
     * Constructor
     * 
     * @param pContext
     *            The bundle context
     */
    public Recorder(final BundleContext pContext) {
        super();

        mContext = pContext;
        mListenersTracker = new ServiceTracker<IProgressListener, IProgressListener>(mContext,
                IProgressListener.class, null) {

            @Override
            public IProgressListener addingService(
                    final ServiceReference<IProgressListener> pReference) {

                final IProgressListener lListener = super.addingService(pReference);
                lListener.init(getRecords(), getStatus().getTargetLevel());

                return lListener;
            }
        };
    }

    /**
     * A bundle has been started in the current level.
     * 
     * @param pBundle
     *            The started bundle
     */
    public void bundleStarted(final Bundle pBundle) {
        mStatus.startBundle(pBundle);
        notifyListeners(false);
    }

    /**
     * Get the init waiting time.
     * <p>
     * This is the initialization time when framework was never launched before.
     * </p>
     * 
     * @return The init waiting time
     */
    public long getInitWaitingTime() {
        return mInitTime;
    }

    /**
     * Get the step waiting time.
     * <p>
     * This is the step time when framework was never launched before.
     * </p>
     * 
     * @return The step waiting time
     */
    public long getStepWaitingTime() {
        return mStepTime;
    }

    /**
     * Get the start level service.
     * 
     * @return The start level service
     */
    public FrameworkStartLevel getStartLevel() {
        return mContext.getBundle(0).adapt(FrameworkStartLevel.class);
    }

    /**
     * Initialize the recorder.
     */
    public void init() {
        mStatus = new StatusImpl((int) getProperty(TARGET_LEVEL_PROPERTY, 1L));

        // Collect first launch times
        mInitTime = getProperty(INIT_TIME_PROPERTY, -1);
        mStepTime = getProperty(STEP_TIME_PROPERTY, -1);
        if (mInitTime < 0) {
            mInitTime = 0;
            if (mStepTime < 0) {
                // If no time is set : collect default time
                mInitTime = getProperty(FELIX_FILEINSTALL_POLL, 2000) * 2;
            }
        }
        if (mStepTime < 0) {
            mStepTime = mInitTime / 10;
        }

        mLaunchLevel = (int) getProperty(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, 2);

        mRecords = loadRecords();

        mListenersTracker.open();

        log(LogService.LOG_INFO, ELaunchMessage.RECORDER_INITIALIZED.getMessage(), null);
    }

    /**
     * Test if the current launch is the first one (no previous data)
     * 
     * @return If it is the first launch.
     */
    public boolean isFirstLaunch() {
        return mRecords.isFirstLaunch();
    }

    /**
     * Stop the recorder.
     */
    public synchronized void stop() {
        notifyListeners(true);
        mListenersTracker.close();
    }

    /**
     * Update to a new start level.
     * 
     * @return If target level has been reached
     */
    public synchronized boolean update() {
        mRecords.addLevel(mStatus.getCurrentLevel(), mStatus.getStartedBundles(),
                mStatus.getSpentTimeInLevel());

        final FrameworkStartLevel lStartLevel = getStartLevel();
        final int lCurrentLevel = lStartLevel.getStartLevel();
        final boolean lFinished = mStatus.getTargetLevel() == lCurrentLevel;

        log(LogService.LOG_DEBUG,
                ELaunchMessage.START_LEVEL_REACHED.getMessage(Integer.valueOf(lCurrentLevel)), null);

        if (lFinished) {

            // Persist data
            mRecords.compile();
            final File lFile = mContext.getDataFile(RECORDS_FILE_NAME);
            try {
                final FileOutputStream lStream = new FileOutputStream(lFile);
                try {
                    lStream.write(mRecords.serialize().getBytes(RECORDS_ENCODING));
                } finally {
                    lStream.close();
                }
                log(LogService.LOG_INFO, ELaunchMessage.LAUNCH_DATA_RECORDED.getMessage(), null);
            } catch (final IOException e) {
                String lPath = NULL;
                if (lFile != null) {
                    lPath = lFile.getAbsolutePath();
                }
                logError(ELaunchMessage.UNABLE_TO_SAVE_RECORDS.getMessage(lPath), e);
            }

        } else if (lCurrentLevel <= mStatus.getTargetLevel()) {

            int lBundleNb = 0;
            for (final Bundle lBundle : mContext.getBundles()) {
                final BundleStartLevel lBundleLvl = lBundle.adapt(BundleStartLevel.class);
                if (lBundleLvl != null && lBundleLvl.getStartLevel() == (lCurrentLevel + 1)
                        && !isFragment(lBundle)) {
                    lBundleNb++;
                }
            }

            mStatus.changeLevel(lCurrentLevel, lBundleNb);
        }

        // Wait for the stop method call for the final notification
        notifyListeners(false);

        return lFinished;
    }

    /**
     * Get the previous records.
     * 
     * @return The records
     */
    RecordsImpl getRecords() {
        return mRecords;
    }

    /**
     * Get the current status.
     * 
     * @return The status
     */
    StatusImpl getStatus() {
        return mStatus;
    }

    /**
     * Extract a long property from the environment properties.
     * 
     * @param pPropertyKey
     *            The property keys
     * @param pDefault
     *            The default value
     * @return The value
     */
    private long getProperty(final String pPropertyKey, final long pDefault) {
        final String lStringValue = mContext.getProperty(pPropertyKey);
        long lValue = pDefault;
        if (lStringValue != null) {
            try {
                lValue = Long.parseLong(lStringValue);
            } catch (final NumberFormatException e) {
                lValue = -1;
            }
            if (lValue < 1) {
                log(LogService.LOG_WARNING,
                        ELaunchMessage.INVALID_PROPERTY.getMessage(pPropertyKey, lStringValue),
                        null);
                lValue = pDefault;
            }
        }
        return lValue;
    }

    /**
     * Test if a bundle is a fragment.
     * 
     * @param pBundle
     *            The bundle to test
     * @return If the bundle is a fragment
     */
    private boolean isFragment(final Bundle pBundle) {
        return pBundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
    }

    /**
     * Load the records from the preferences
     * 
     * @return The previous recorded data.
     */
    private RecordsImpl loadRecords() {
        String lSerializedRecords = RecordsImpl.EMPTY_RECORDS;

        // Load
        final File lFile = mContext.getDataFile(RECORDS_FILE_NAME);
        try {
            if (lFile.exists()) {
                final FileInputStream lStream = new FileInputStream(lFile);
                try {
                    final StringBuilder lBuilder = new StringBuilder();
                    final byte[] lData = new byte[1024];
                    int lRead = lStream.read(lData);
                    while (lRead != -1) {
                        lBuilder.append(new String(lData, 0, lRead, RECORDS_ENCODING));
                        lRead = lStream.read(lData);
                    }
                    if (lBuilder.length() > 0) {
                        lSerializedRecords = lBuilder.toString();
                    }
                } finally {
                    lStream.close();
                }
                log(LogService.LOG_INFO, ELaunchMessage.RECORDS_LOADED.getMessage(), null);
            }
        } catch (final IOException e) {
            String lPath = NULL;
            if (lFile != null) {
                lPath = lFile.getAbsolutePath();
            }
            logError(ELaunchMessage.UNABLE_TO_LOAD_RECORDS.getMessage(lPath), e);
        }

        // Deserialize
        try {
            return new RecordsImpl(mInitTime, mStepTime, mLaunchLevel, mStatus.getTargetLevel(),
                    lSerializedRecords);
        } catch (final ParseException e) {
            // Reset stats
            return new RecordsImpl(mInitTime, mStepTime, mLaunchLevel, mStatus.getTargetLevel());
        }
    }

    /**
     * Log a message.
     * 
     * @param pLevel
     *            The log level
     * 
     * @param pMessage
     *            The message to log
     * @param pException
     *            The exception to log
     */
    void log(final int pLevel, final String pMessage, final Exception pException) {
        final ServiceReference<LogService> lLogRef = mContext.getServiceReference(LogService.class);
        if (lLogRef != null) {
            final LogService lService = mContext.getService(lLogRef);
            if (lService != null) {
                try {
                    lService.log(pLevel, pMessage, pException);
                } finally {
                    mContext.ungetService(lLogRef);
                }
            }
        }
    }

    /**
     * Log an error message.
     * 
     * @param pMessage
     *            The message to log
     * @param pException
     *            The exception to log
     */
    void logError(final String pMessage, final Exception pException) {
        log(LogService.LOG_ERROR, pMessage, pException);
    }

    /**
     * Notify the listeners of the current progress.
     * 
     * @param pEnded
     *            If launch is ended
     */
    private void notifyListeners(final boolean pEnded) {
        final Object[] lServices = mListenersTracker.getServices();
        if (lServices != null) {
            for (final Object lService : lServices) {
                final IProgressListener lListener = (IProgressListener) lService;
                if (lListener != null) {
                    // Ended
                    if (pEnded) {
                        lListener.ended();
                    }
                    // Progress
                    else {
                        lListener.progressChanged(mStatus);
                    }
                }
            }
        }
    }
}
